package com.jinglin.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jinglin.component.EsSearchComponent;
import com.jinglin.component.RedisComponent;
import com.jinglin.entity.config.AppConfig;
import com.jinglin.entity.constants.Constants;
import com.jinglin.entity.dto.SysSettingDto;
import com.jinglin.entity.dto.TokenUserInfoDto;
import com.jinglin.entity.enums.PageSize;
import com.jinglin.entity.enums.ResponseCodeEnum;
import com.jinglin.entity.enums.UserActionTypeEnum;
import com.jinglin.entity.enums.VideoOrderTypeEnum;
import com.jinglin.entity.po.*;

import com.jinglin.entity.query.SimplePage;
import com.jinglin.entity.query.UserActionQuery;
import com.jinglin.entity.query.VideoInfoQuery;
import com.jinglin.entity.vo.PaginationResultVO;
import com.jinglin.entity.vo.VideoInfoResultVo;
import com.jinglin.exception.BusinessException;
import com.jinglin.mapper.UserActionMapper;
import com.jinglin.mapper.UserInfoMapper;
import com.jinglin.mapper.VideoInfoMapper;
import com.jinglin.redis.RedisData;
import com.jinglin.redis.RedisUtils;
import com.jinglin.service.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.File;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * <p>
 * 视频信息 服务实现类
 * </p>
 *
 * @author ${author}
 * @since 2025-03-12
 */
@Service
@Slf4j
public class VideoInfoServiceImpl extends ServiceImpl<VideoInfoMapper, VideoInfo> implements VideoInfoService {
    @Autowired
    private RedisUtils redisUtils;
    @Autowired
    private VideoInfoMapper videoInfoMapper;
    @Autowired
    @Lazy
    private VideoInfoPostService videoInfoPostService;
    @Autowired
    private RedisComponent redisComponent;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private UserInfoMapper userInfoMapper;
    @Autowired
    private VideoInfoFileService videoInfoFileService;
    @Autowired
    @Lazy
    private VideoInfoFilePostService videoInfoFilePostService;
    @Autowired
    @Lazy
    private VideoCommentService videoCommentService;
    @Autowired
    @Lazy
    private VideoDanmuService videoDanmuService;
    @Autowired
    private AppConfig appConfig;
/*    @Autowired
    private EsSearchComponent esSearchComponent;*/
    @Autowired
    private UserActionMapper userActionMapper;

    private ExecutorService executorService = Executors.newFixedThreadPool(10);
    private ExecutorService videoInfoServiceExecutor = Executors.newFixedThreadPool(10);

    @Override
    public Integer reportVideoPlayOnline(String fileId, String deviceId) {
        // 格式化key
        // 保存用户在线信息的key
        String userPlayOnlineKey = String.format(Constants.REDIS_KEY_VIDEO_PLAY_COUNT_USER, fileId, deviceId);
        // 保存在线人数的key
        String playOnlineCountKey = String.format(Constants.REDIS_KEY_VIDEO_PLAY_COUNT_ONLINE, fileId);

        // 如果用户在线信息不存在。
        if (!redisUtils.keyExists(userPlayOnlineKey)) {
            // 则新增,过期时间为8秒。
            redisUtils.setex(userPlayOnlineKey, fileId, Constants.REDIS_KEY_EXPIRES_ONE_SECONDS * 8);
            // 同时自增一下在线人数。incrementex这个方法会创建一个key，如果key不存在，初始化其值为0。
            // 返回值是自增后的值
            return redisUtils.incrementex(playOnlineCountKey, Constants.REDIS_KEY_EXPIRES_ONE_SECONDS * 10).intValue();
        }
        // 如果存在。
        //给视频在线总数量续期
        redisUtils.expire(playOnlineCountKey, Constants.REDIS_KEY_EXPIRES_ONE_SECONDS * 10);
        //给播放用户续期
        redisUtils.expire(userPlayOnlineKey, Constants.REDIS_KEY_EXPIRES_ONE_SECONDS * 8);
        Integer count = (Integer) redisUtils.get(playOnlineCountKey);
        return count == null ? 1 : count;
    }

    @Override
    @Transactional
    public PaginationResultVO<VideoInfo> loadVideoList(Integer pageNo, Integer pageSize, Integer orderType, String videoName, String userId) {
        if (pageNo == null) {
            pageNo = 1;
        }
        if (pageSize == null) {
            pageSize = 15;
        }
        LambdaQueryWrapper<VideoInfo> countWrapper = new LambdaQueryWrapper<>();
        countWrapper.eq(VideoInfo::getUserId, userId);
        int count = this.count(countWrapper);
        VideoOrderTypeEnum videoOrderTypeEnum = VideoOrderTypeEnum.getByType(orderType);
        List<VideoInfo> list = videoInfoMapper.getVideoInfoListPage(pageNo - 1, pageSize, videoOrderTypeEnum.getField(), videoName, userId);

        SimplePage page = new SimplePage(pageNo, count, pageSize);
        PaginationResultVO<VideoInfo> resultVO = new PaginationResultVO<>(count, pageSize, pageNo, page.getPageTotal(), list);


        return resultVO;
    }

    @Override
    @Transactional
    public void deleteVideo(String videoId, String userId) {
        VideoInfoPost videoInfoPost = videoInfoPostService.getById(videoId);
        if (videoInfoPost == null || userId != null && !userId.equals(videoInfoPost.getUserId())) {
            throw new BusinessException(ResponseCodeEnum.CODE_404);
        }
        // 删除信息表
        videoInfoPostService.removeById(videoId);
        this.removeById(videoId);

        /*
         * 删除用户硬币
         * */
        SysSettingDto sysSettingDto = redisComponent.getSysSettingDto();
        userInfoMapper.updateCoinCountInfo(userId, -sysSettingDto.getPostVideoCoinCount());
/*        *//*
         * 删除es信息
         * *//*
        esSearchComponent.delDoc(videoId);*/
        // 在异步线程中，删除 分片、弹幕、评论、文件信息
        executorService.execute(() -> {
            LambdaQueryWrapper<VideoInfoFile> fileWrapper = new LambdaQueryWrapper<>();
            // 查询分片(文件)
            fileWrapper.eq(VideoInfoFile::getVideoId, videoId);
            List<VideoInfoFile> fileList = videoInfoFileService.list(fileWrapper);

            //删除文件分片信息
            videoInfoFileService.remove(fileWrapper);
            LambdaQueryWrapper<VideoInfoFilePost> filePostWrapper = new LambdaQueryWrapper<>();
            filePostWrapper.eq(VideoInfoFilePost::getVideoId, videoId);
            videoInfoFilePostService.remove(filePostWrapper);

            // 删除弹幕
            LambdaQueryWrapper<VideoDanmu> danmuWrapper = new LambdaQueryWrapper<>();
            danmuWrapper.eq(VideoDanmu::getVideoId, videoId);
            videoDanmuService.remove(danmuWrapper);

            // 删除评论
            LambdaQueryWrapper<VideoComment> commentWrapper = new LambdaQueryWrapper<>();
            commentWrapper.eq(VideoComment::getVideoId, videoId);
            videoCommentService.remove(commentWrapper);

            // 删除文件
            for (VideoInfoFile videoInfoFile : fileList) {
                String filePath = videoInfoFile.getFilePath();
                File file = new File(appConfig.getProjectFolder() + Constants.FILE_FOLDER + filePath);
                try {
                    FileUtils.deleteDirectory(file);
                    log.info("删除文件成功，文件路径：{}", file.getAbsolutePath());
                } catch (IOException e) {
                    log.error("删除文件失败，文件路径：{}", file.getAbsolutePath());
                }

            }

        });


    }

    @Override
    public void addReadCount(String videoId) {
        LambdaUpdateWrapper<VideoInfo> wrapper = new LambdaUpdateWrapper<>();
        wrapper.eq(VideoInfo::getVideoId, videoId);
        wrapper.setSql("play_count = play_count + 1");
        this.update(wrapper);
    }

    @Override
    public PaginationResultVO<VideoInfo> loadHotVideoList(Integer pageNo) {
        Integer count = videoInfoMapper.count24HotVideo();
        int pageSize = 15;

        SimplePage page = new SimplePage(pageNo, count, pageSize);

        List<VideoInfo> videoInfos = videoInfoMapper.select24HotVideoList(page.getStart(), Constants.LENGTH_15);


        PaginationResultVO<VideoInfo> result = new PaginationResultVO(count, page.getPageSize(), page.getPageNo(), page.getPageTotal(), videoInfos);


        return result;
    }

    @Override
    @Transactional
    public VideoInfoResultVo getVideoInfo(String videoId, TokenUserInfoDto tokenUserInfoDto) {
        // 我并没有对用户行为进行缓存。
        // 因为我认为同一个用户点击同一个视频这样的操作是非常少数的。没必要建立缓存
        VideoInfo videoInfo = getVideoInfoWithLogicTime(videoId);

        VideoInfoResultVo videoInfoResultVo = new VideoInfoResultVo();

        //获取当前本用户行为：点赞、收藏、投币、评论（此条视频是否点赞、收藏、投币？）
        // 如果有数据的话，前端可以方便针对用户做数据回显。比如点赞和收藏投币按钮亮起来。如果没有的话则是熄灭状态
        if (tokenUserInfoDto != null) {
            UserActionQuery userActionQuery = new UserActionQuery();
            userActionQuery.setVideoId(videoId);
            userActionQuery.setUserId(tokenUserInfoDto.getUserId());
            // 设置三种状态。到时候通过forEach循环查询三次。得到三条sql
            userActionQuery.setActionTypeArray(new Integer[]{UserActionTypeEnum.VIDEO_LIKE.getType(), UserActionTypeEnum.VIDEO_COLLECT.getType(), UserActionTypeEnum.VIDEO_COIN.getType()});
            List<UserAction> userActions = userActionMapper.queryActionTypeList(userActionQuery);
            videoInfoResultVo.setUserActionList(userActions);
        }
        videoInfoResultVo.setVideoInfo(videoInfo);
        return videoInfoResultVo;
    }

    @Override
    public PaginationResultVO<VideoInfo> searchVideos(String keyword, Integer orderType, Integer pageNo, Integer pageSize) {

        // 记录搜索关键词到Redis
        redisComponent.addKeywordCount(keyword);

        // 转换关键词为布尔搜索格式
        String searchTerms = convertToBooleanSearch(keyword);

        // 创建分页对象
        Page<VideoInfo> page = new Page<>(pageNo, pageSize);

        // 构建查询条件
        LambdaQueryWrapper<VideoInfo> wrapper = new LambdaQueryWrapper<>();


        // 添加排序条件 - JDK 11兼容写法
        if (orderType != null) {
            switch (orderType) {
                case 1: // 最新
                    wrapper.orderByDesc(VideoInfo::getCreateTime);
                    break;
                case 2: // 最热
                    wrapper.orderByDesc(VideoInfo::getPlayCount);
                    break;
                case 3: // 推荐
                    wrapper.orderByDesc(VideoInfo::getRecommendType);
                    break;
                // 默认按相关性排序，在XML中处理
                default:
                    break;
            }
        }

        // 执行搜索
        IPage<VideoInfo> resultPage = videoInfoMapper.fullTextSearch(
                page, searchTerms, wrapper);

        // 转换为自定义分页结果
        return convertToPaginationResult(resultPage);
    }
    private String convertToBooleanSearch(String keyword) {
        if (StringUtils.isBlank(keyword)) {
            return "";
        }
        return Arrays.stream(keyword.trim().split("\\s+"))
                .filter(StringUtils::isNotBlank)
                .map(term -> "+" + term + "*")
                .collect(Collectors.joining(" "));
    }

    private PaginationResultVO<VideoInfo> convertToPaginationResult(IPage<VideoInfo> page) {
        PaginationResultVO<VideoInfo> resultVO = new PaginationResultVO<>();
        resultVO.setList(page.getRecords());
        resultVO.setPageNo((int) page.getCurrent());
        resultVO.setPageSize((int) page.getSize());
        resultVO.setPageTotal((int) page.getTotal());
        return resultVO;
    }




    /**
    * @author:jinglin
    * 对于逻辑过期这种数据处理方式，我们一定要提前导入缓存，不然到时候拿到的videoInfo都是空的。很多功能都会报错.我在测试方法里写的有统一导入缓存的方法
    */
    public VideoInfo getVideoInfoWithLogicTime(String videoId) {

        // 建立缓存，为了避免缓存击穿。采用设置逻辑时间方式
        RedisData redisData = (RedisData) redisUtils.get(Constants.REDIS_KEY_VIDEO_INFO_KEY + videoId);

        if (redisData == null) {
            // 如果为空。则直接返回空。因为如果这里为空，则数据库和redis中都没有数据。（我们在使用之前会将数据全部都导入到redis中）
            return null;
        }

        // 不为空，判断逻辑时间是否过期
        if (redisData.getExpireTime().isBefore(LocalDateTime.now())) {
            // 如果过期，
            // 拿锁。
            boolean flag = tryLock(Constants.REDIS_KEY_VIDEO_INFO_LOCK + videoId);

            if (!flag) {
                // 拿不到锁，就先休眠，再重试
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                return getVideoInfoWithLogicTime(videoId);
            }
            // 拿到锁了
            executorService.execute(() -> {
                try {
                    // 如果拿到锁，就去执行异步线程，建立缓存
                    VideoInfo videoInfo = this.getById(videoId);
                    if (videoInfo != null) {

                        buildVideoInfoCache(videoId);
                    } else {
                        // 如果没有。我们将空值纳入缓存（其实不可能为空。因为在实际业务中这是一个热点key，高点击量的东西）
                        // 为了健壮性，我们做缓存空值防止缓存穿透。
                        cacheNullValue(videoId);
                    }
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    unLock(Constants.REDIS_KEY_VIDEO_INFO_LOCK + videoId);
                }
            });
        }

        VideoInfo data = (VideoInfo) redisData.getData();
        return data;
    }

    private void cacheNullValue(String videoId) {
        RedisData redisData = new RedisData();
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(10L));
        redisData.setData(null);
        redisUtils.set(Constants.REDIS_KEY_VIDEO_INFO_KEY + videoId, redisData);

    }

    public void buildVideoInfoCache(String videoId) {
        VideoInfo videoInfo = this.getById(videoId);
        RedisData redisData = new RedisData();
        // 避免大量的缓存同时重建（跟传统的缓存雪崩有异曲同工之妙）
        //  大量的缓存同时过期。然后分别去竞争各自的锁的同时，又有很多线程sleep200ms，影响性能。虽然不像ttl那么严重。
        // 因此我们给过期时间设置5个偏移量。
        long randomOffset = ThreadLocalRandom.current().nextInt(5);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(10L + randomOffset));

        redisData.setData(videoInfo);
        redisUtils.set(Constants.REDIS_KEY_VIDEO_INFO_KEY + videoId, redisData);
    }

    public boolean tryLock(String key) {
        // 设置成功则返回true
        return redisTemplate.opsForValue().setIfAbsent(key, "", 10, TimeUnit.SECONDS);
    }

    public void unLock(String key) {
        redisTemplate.delete(key);
    }


}
